package cn.yeziji.forum.utils;

import cn.yeziji.forum.common.key.UserCacheKey;
import cn.yeziji.forum.common.key.generate.UserCacheKeyGenerate;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Jwt 工具类
 *
 * @author gzkemays
 */
public class JwtUtils {
  private String subject;
  private String secret;
  private String roleClams;
  private String header;
  private String prefix;
  private long expired = 1000 * 24 * 60 * 60 * 7;
  //  private long expired = 1000 * 30;
  CacheUtils cache = SpringUtils.getBean(CacheUtils.class);

  private interface Builder {
    /**
     * 初始化参数
     *
     * @param params 参数
     * @return JwtUtils
     */
    JwtUtils init(String... params);
  }

  private JwtUtils() {}

  private JwtUtils(JwtUtilBuilder builder) {
    subject = builder.subject;
    secret = builder.secret;
    header = builder.header;
    prefix = builder.prefix;
    roleClams = builder.roleClams;
    expired = builder.expired;
  }

  public static class JwtUtilBuilder implements Builder {
    private String subject = "yeizjigroup-subject";
    private String secret = "yeizjigroup-secret";
    private String header = HttpHeaders.AUTHORIZATION;
    private String prefix = "Bearer";
    private String roleClams = "roles";
    private long expired = 1000 * 24 * 60 * 60 * 7;

    private JwtUtilBuilder() {}

    @Override
    public JwtUtils init(String... params) {
      for (int i = 0; i < params.length; i++) {
        switch (i) {
          case 1:
            this.secret = Optional.of(params[1]).orElse("yezijigroup-secret");
            break;
          case 2:
            this.header = Optional.of(params[2]).orElse(HttpHeaders.AUTHORIZATION);
            break;
          case 3:
            this.prefix = Optional.of(params[3]).orElse("Bearer");
            break;
          case 4:
            this.roleClams = Optional.of(params[4]).orElse("roles");
            break;
          case 5:
            this.expired =
                Optional.of(Long.parseLong(params[5])).orElse((long) 1000 * 24 * 60 * 60 * 7);
            break;
          default:
            this.subject = Optional.of(params[0]).orElse("yezijigroup-subject");
        }
      }
      return new JwtUtils(this);
    }
  }

  public static JwtUtilBuilder settings() {
    return new JwtUtilBuilder();
  }

  public <T> String generateJsonWebToken(Object authorities, Object id, Object username) {
    return Jwts.builder()
        .setSubject(subject)
        .claim(roleClams, authorities)
        .claim("id", id)
        .claim("username", username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + expired))
        .signWith(SignatureAlgorithm.HS256, secret)
        .compact();
  }

  /**
   * 生成token
   *
   * @param username 用户名
   * @param role 角色定位
   * @return token
   */
  public String createToken(String username, String role) {
    Map<String, Object> map = new HashMap<>(1);
    map.put(roleClams, role);
    return Jwts.builder()
        .setSubject(username)
        .setClaims(map)
        .claim("username", username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + expired))
        .signWith(SignatureAlgorithm.HS256, secret)
        .compact();
  }

  /**
   * 检测 jwt
   *
   * <ul>
   *   过期续期策略
   *   <li>每次使用 jwt 时都会调用此方法，当 token 过期时则会进入 {@link ExpiredJwtException} 异常，从而在捕获异常后对 token 进行续期补救
   *   <li>首次过期时会从缓存中获取过期的 token，而后根据 token 的数据生成新的 token，同时生成一个 {@link
   *       UserCacheKeyGenerate#getRefreshTokenKey} 用于记录旧 token 对应的新 token。
   *   <li>生成新 token 记录时，因为 {@code JwtAuthFilter} 中 request 依旧使用旧的 token
   *       进行操作，因此它在做权限过滤时，需要判断记录是否存在，如果存在则将新的 token 插入 response 响应头中给客户端更换 token。
   *   <li>在业务逻辑当中，因为当 token 过期时，发送的请求仍然是旧的 token，那么业务逻辑依然会走过期异常的流程，所以追加判断 {@code obj instanceof
   *       JSONObject} 用于查看是否<strong>第一次使用</strong>已过期 token，如果是的话，那么就调用{@link
   *       UserCacheKeyGenerate#getRefreshTokenKey} 的缓存记录进行逻辑调用而后再删除，从而达到旧 token 可以进行最后一次调用。
   * </ul>
   *
   * <p style="color: red; font-weight: 800">PS：因为该策略是可以让过期的 token 实现最后一次调用，那么调用后必须放入 response
   * 当中返回给客户端，让客户端替换掉旧的 token，否则后续请求中客户端就无法获取刷新的 token 导致续期失败。
   *
   * @param token token信息
   * @return 声明信息
   */
  public Claims checkJwt(String token) {
    UserCacheKeyGenerate generate = UserCacheKey.GENERATE;
    // 先查 redis ，如果 redis 的 token 已经不存在则被视为过期
    String tokenCacheKey = UserCacheKey.GENERATE.getTokenCacheKey(token);
    Object redisToken = cache.getRedisTemplate().opsForValue().get(tokenCacheKey);
    if (redisToken != null) {
      return Jwts.claims((JSONObject) redisToken);
    }
    try {
      // 如果 token 过期会在这里捕获异常
      return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    } catch (ExpiredJwtException expired) {
      // token 检测失败意味着 token 失效
      String oldTokenKey;
      Object obj = cache.get(oldTokenKey = generate.getTokenCacheKey(token));
      if (obj instanceof JSONObject) {
        JSONObject json = (JSONObject) obj;
        String username = json.getString("username");
        // 新的 token 属性
        String newTokenCacheKey = generate.getRefreshTokenKey(token), newToken;
        // 没有新的 token 缓存，则在 cache 中创建新 token 缓存。
        if ((newToken = cache.get(newTokenCacheKey)) == null) {
          newToken = JwtOperaUtils.generateJsonWebTokenByJson(json);
          // 删除旧的 token 缓存，新增新的 token 缓存。
          cache.remove(oldTokenKey);
          cache.set(generate.getTokenCacheKey(newToken), json, 30, TimeUnit.DAYS);
          cache.set(newTokenCacheKey, newToken, 30, TimeUnit.DAYS);
          // 刷新次要 token
          cache.set(generate.getSecondaryTokenKey(username), newToken, 30, TimeUnit.DAYS);
        }
        // 用新的 token 做 jwt 解析并返回
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(newToken).getBody();
      } else {
        // token 无效，那么尝试获取是否有新的 token
        String newToken = cache.get(generate.getRefreshTokenKey(token));
        if (StringUtils.isNotBlank(newToken) && !token.equals(newToken)) {
          cache.remove(generate.getRefreshTokenKey(token));
          return Jwts.parser().setSigningKey(secret).parseClaimsJws(newToken).getBody();
        }
      }
      return null;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * 获取用户名
   *
   * @param token token 信息
   * @return 用户名【插入 claims 之中】
   */
  public String getUsername(String token) {
    Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    return claims.get("username").toString();
  }

  /**
   * 获取用户角色
   *
   * @param token token 信息
   * @return 用户角色定位
   */
  @SuppressWarnings("all")
  public List<SimpleGrantedAuthority> getUserRole(String token) {
    Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    List roles = (List) claims.get(roleClams);
    String json = JSONArray.toJSONString(roles);
    List<SimpleGrantedAuthority> grantedAuthorityList =
        JSONArray.parseArray(json, SimpleGrantedAuthority.class);
    return grantedAuthorityList;
  }

  /**
   * 是否过期
   *
   * @param token token信息
   * @return 判断 expire 是否过期
   */
  public boolean isExpiration(String token) {
    Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    return claims.getExpiration().before(new Date());
  }

  public static void main(String[] args) throws ParseException {
    Date date = new Date();
    String day = date.toString();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    String format = dateFormat.format(date);

    SimpleDateFormat dateFormat1 =
        new SimpleDateFormat("EEE MMM d HH:mm:ss 'CST' yyyy", Locale.ENGLISH);
    Date parse = dateFormat1.parse(day);
  }
}
